package de.dailab.graphworld.bean;

import de.dailab.graphworld.ontology.AgentType;
import de.dailab.graphworld.ontology.BrokeringProtocol;
import de.dailab.graphworld.ontology.GraphWorldException;
import de.dailab.graphworld.ontology.RegistrationRequest;
import de.dailab.graphworld.ontology.Registration;
import de.dailab.graphworld.ontology.Task;
import de.dailab.graphworld.ontology.Node;
import de.dailab.graphworld.ontology.Reply;
import de.dailab.graphworld.world.GraphWorldAgentBean;
import de.dailab.jiactng.agentcore.action.Action;
import de.dailab.jiactng.agentcore.action.ActionResult;
import de.dailab.jiactng.agentcore.comm.IGroupAddress;
import de.dailab.jiactng.agentcore.comm.message.JiacMessage;
import de.dailab.jiactng.agentcore.knowledge.IFact;
import de.dailab.jiactng.agentcore.ontology.IActionDescription;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.Set;

/**
 *
 * @author Kseniya
 */
public class BeanScout extends BeanPhysical {

    public static final String GROUP_NAME = "scouts";
    private IActionDescription actionExplore;
    private IActionDescription actionMove;
    private final Queue<Node> moves = new LinkedList<>();
    private final List<Task> tasks = new LinkedList<>();

    @Override
    public void doStart() {
        super.doStart();
        actionExplore = thisAgent.searchAction(new Action(GraphWorldAgentBean.ACTION_EXPLORE));
        actionMove = thisAgent.searchAction(new Action(GraphWorldAgentBean.ACTION_MOVE));
    }

    @Override
    public void execute() {
        super.execute();
        if (!tasks.isEmpty()) {
            delegateTask(tasks.iterator().next());
        }
        // do the next move if defined or explore to get the next move
        if (!moves.isEmpty()) {
            move(moves.peek());
        }
//        System.out.println("tasks: " + tasks.toString());
    }

    @Override
    public void receiveResult(ActionResult result) {
        String actionName = result.getAction().getName();
        if (result.getFailure() != null) {
            log.error(actionName);
        } else {
            switch (actionName) {
                case GraphWorldAgentBean.ACTION_INIT_TEAM_POSITION:
                    Node curr = (Node) result.getResults()[0];
                    memory.write(curr);
                    explore();
                    break;
                case GraphWorldAgentBean.ACTION_EXPLORE:
                    Set<Node> neighbours = new HashSet<>((Set<Node>) result.getResults()[0]);
                    consumeExplore(neighbours);
                    break;
                case GraphWorldAgentBean.ACTION_MOVE:
                    Node movedTo = (Node) result.getResults()[0];
                    moves.poll();
                    if (memory.read(new Node(null)) == null) {
                        memory.write(movedTo);
                    } else {
                        memory.update(new Node(null), movedTo);
                    }
                    explore();
                    break;
                default:
                    break;
            }
        }
        super.receiveResult(result);
    }

    private void explore() {
        this.invoke(actionExplore, null, this);
    }

    private void queueNextMove(Node node) {
        moves.offer(node);
    }

    private void move(Node target) {
        this.invoke(actionMove, new Serializable[]{target}, this);
    }

    /**
     * should be called after successful explore only. Updates memory, selects
     * next node to move to and queues an appropriate move action.
     *
     * @param explored set of explored nodes
     */
    private void consumeExplore(Set<Node> explored) {
        // update tasks using new information we received
        updateTasks(explored);
        // select next move randomly
        Node selected = selectMoveTarget(explored);
        // queue next move
        queueNextMove(selected);
    }

    /**
     * updates tasks based on passed nodes information. Removes outdated tasks,
     * updates existing ones with new gold amounts and resorts tasks so the most
     * important ones (with most gold) are delegated first.
     *
     * @param nodes new nodes information
     */
    private void updateTasks(Collection<Node> nodes) {
        List<Node> nodesWithNoTask = new ArrayList<>(nodes.size());
        for (Node node : nodes) {
            boolean taskFound = false;
            Iterator<Task> itr = tasks.iterator();
            while (itr.hasNext()) {
                Task nextTask = itr.next();
                // a task referring that node was found, must be (eventually) updated
                if (nextTask.goldNode.getID() == node.getID()) {
                    taskFound = true;
                    // if no gold on node at all, the task should not be delegated anymore
                    if (node.getGold() == 0) {
                        itr.remove();
                    } else {
                        // update the gold amount in task
                        nextTask.goldNode = node;
                    }
                }
            }
            // no task found, but there is gold on node
            if (!taskFound && node.getGold() > 0) {
                nodesWithNoTask.add(node);
            }
        }
        for (Node node : nodesWithNoTask) {
            // store the task to delegate it later
            tasks.add(new Task(node));
        }

        // sort by gold amount to shift move important tasks to the front
        Collections.sort(tasks, new Comparator<Task>() {
            @Override
            public int compare(Task o1, Task o2) {
                return (-1) * o1.goldNode.getGold().compareTo(o2.goldNode.getGold());
            }
        });
    }

    private Node selectMoveTarget(Collection<Node> nodes) {
        Node current = memory.read(new Node(null));
        Iterator<Node> it = nodes.iterator();
        while (it.hasNext()) {
            Node node = it.next();
            if (node.getID() == current.getID()) {
                it.remove();
            }
        }
        return nodes.iterator().next();
    }

    private void delegateTask(Task task) {
        sendMsgGroup(task, AgentType.SCOUT.getAddress());
    }

    @Override
    protected boolean isInitialisedInWorld() {
        Node storedPosition = memory.read(new Node(null));
        return storedPosition != null;
    }

    @Override
    protected Collection<IGroupAddress> getGroups() {
        return Collections.singleton(AgentType.SCOUT.getAddress());
    }

    @Override
    public void consume(JiacMessage msg) {
        IFact payload = msg.getPayload();
        // broker checks who's alive, reply him
        if (payload instanceof RegistrationRequest) {
            sendMsgPersonal(new Registration(thisAgent.getAgentDescription(), AgentType.SCOUT), msg.getSender());
        } else if (payload instanceof Reply) {
            Reply reply = (Reply) payload;
            BrokeringProtocol type = (BrokeringProtocol) reply.getType();
//            System.out.println("broker replied " + type + " on task " + reply.getTask());
            switch (type) {
                // positive answers
                case AGREE:
                    break;
                case SUBPROTOCOL_PROCEED:
                case INFORM_DONE_PROXY:
                    // remove all queued tasks that refer to the same gold node
                    updateTasks(Collections.singleton(reply.getTask().goldNode));
                    break;
                // negative answers
                case REFUSE:
                case FAILURE_NO_MATCH:
                case FAILURE_PROXY:
                    // do nothing so the current task can be suggested once again
                    break;
                case SUBPROTOCOL_FAILURE:
                    // remove all queued tasks that refer to the same gold node
                    updateTasks(Collections.singleton(reply.getTask().goldNode));
                    break;
                default:
                    break;
            }
        }
    }
}
